using System.Text;
using System.Text.Json;
using CSharpier.Core.CSharp.SyntaxPrinter;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CSharpier.Core.CSharp;

public static class CSharpFormatter
{
    private static readonly JsonSerializerOptions IndentedJson = new() { WriteIndented = true };
    internal static readonly LanguageVersion LanguageVersion = LanguageVersion.Preview;

    public static CodeFormatterResult Format(string code, CodeFormatterOptions? options = null)
    {
        return FormatAsync(code, options).Result;
    }

    public static Task<CodeFormatterResult> FormatAsync(
        string code,
        CodeFormatterOptions? options = null,
        CancellationToken cancellationToken = default
    )
    {
        return FormatAsync(code, (options ?? new()).ToPrinterOptions(), cancellationToken);
    }

    public static CodeFormatterResult Format(
        SyntaxTree syntaxTree,
        CodeFormatterOptions? options = null
    )
    {
        return FormatAsync(syntaxTree, options).Result;
    }

    public static Task<CodeFormatterResult> FormatAsync(
        SyntaxTree syntaxTree,
        CodeFormatterOptions? options = null,
        CancellationToken cancellationToken = default
    )
    {
        return FormatAsync(
            syntaxTree,
            (options ?? new()).ToPrinterOptions(),
            SourceCodeKind.Regular,
            cancellationToken
        );
    }

    internal static Task<CodeFormatterResult> FormatAsync(
        string code,
        PrinterOptions printerOptions
    ) => FormatAsync(code, printerOptions, CancellationToken.None);

    internal static Task<CodeFormatterResult> FormatAsync(
        string code,
        PrinterOptions printerOptions,
        CancellationToken cancellationToken
    ) => FormatAsync(code, printerOptions, SourceCodeKind.Regular, cancellationToken);

    internal static Task<CodeFormatterResult> FormatAsync(
        string code,
        PrinterOptions printerOptions,
        SourceCodeKind sourceCodeKind,
        CancellationToken cancellationToken
    )
    {
        var initialSymbolSet = Array.Empty<string>();

        return FormatAsync(
            ParseText(code, initialSymbolSet, sourceCodeKind, cancellationToken),
            printerOptions,
            sourceCodeKind,
            cancellationToken
        );
    }

    private static SyntaxTree ParseText(
        string codeToFormat,
        IEnumerable<string> preprocessorSymbols,
        SourceCodeKind sourceCodeKind,
        CancellationToken cancellationToken
    )
    {
        return CSharpSyntaxTree.ParseText(
            codeToFormat,
            new CSharpParseOptions(
                LanguageVersion,
                DocumentationMode.Diagnose,
                preprocessorSymbols: preprocessorSymbols,
                kind: sourceCodeKind
            ).WithFeatures([new KeyValuePair<string, string>("FileBasedProgram", "true")]),
            cancellationToken: cancellationToken
        );
    }

    internal static async Task<CodeFormatterResult> FormatAsync(
        SyntaxTree syntaxTree,
        PrinterOptions printerOptions,
        SourceCodeKind sourceCodeKind,
        CancellationToken cancellationToken
    )
    {
        var syntaxNode = await syntaxTree.GetRootAsync(cancellationToken);
        if (syntaxNode is not CompilationUnitSyntax rootNode)
        {
            throw new Exception(
                "Root was not CompilationUnitSyntax, it was " + syntaxNode.GetType()
            );
        }

        if (
            !printerOptions.IncludeGenerated
            && GeneratedCodeUtilities.BeginsWithAutoGeneratedComment(rootNode)
        )
        {
            return new CodeFormatterResult { Code = syntaxTree.ToString() };
        }

        bool TryGetCompilationFailure(out CodeFormatterResult compilationResult)
        {
            var diagnostics = syntaxTree
                .GetDiagnostics(cancellationToken)
                .Where(o => o.Severity == DiagnosticSeverity.Error && o.Id != "CS1029")
                .ToList();
            if (diagnostics.Count != 0)
            {
                compilationResult = new CodeFormatterResult
                {
                    Code = syntaxTree.ToString(),
                    CompilationErrors = diagnostics,
                    AST = printerOptions.IncludeAST ? PrintAST(rootNode) : string.Empty,
                };

                return true;
            }

            compilationResult = CodeFormatterResult.Null;
            return false;
        }

        if (TryGetCompilationFailure(out var result))
        {
            return result;
        }

        try
        {
            var lineEnding = PrinterOptions.GetLineEnding(syntaxTree.ToString(), printerOptions);
            var printingContext = new PrintingContext
            {
                Options = new PrintingContext.PrintingContextOptions
                {
                    LineEnding = lineEnding,
                    IndentSize = printerOptions.IndentSize,
                    UseTabs = printerOptions.UseTabs,
                },
            };
            var document = Node.Print(rootNode, printingContext);
            var formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding);
            var reorderedModifiers = printingContext.State.ReorderedModifiers;
            var reorderedUsingsWithDisabledText = printingContext
                .State
                .ReorderedUsingsWithDisabledText;
            var movedTrailingTrivia = printingContext.State.MovedTrailingTrivia;

            foreach (var symbolSet in PreprocessorSymbols.GetSets(syntaxTree))
            {
                syntaxTree = ParseText(formattedCode, symbolSet, sourceCodeKind, cancellationToken);

                if (TryGetCompilationFailure(out result))
                {
                    return result;
                }

                var formattingContext2 = new PrintingContext
                {
                    Options = new PrintingContext.PrintingContextOptions
                    {
                        LineEnding = lineEnding,
                        IndentSize = printerOptions.IndentSize,
                        UseTabs = printerOptions.UseTabs,
                    },
                };
                document = Node.Print(
                    await syntaxTree.GetRootAsync(cancellationToken),
                    formattingContext2
                );
                formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding);
                reorderedModifiers =
                    reorderedModifiers || formattingContext2.State.ReorderedModifiers;
                reorderedUsingsWithDisabledText =
                    reorderedUsingsWithDisabledText
                    || formattingContext2.State.ReorderedUsingsWithDisabledText;
                movedTrailingTrivia =
                    movedTrailingTrivia || formattingContext2.State.MovedTrailingTrivia;
            }

            return new CodeFormatterResult
            {
                Code = formattedCode,
                DocTree = printerOptions.IncludeDocTree
                    ? DocSerializer.Serialize(document)
                    : string.Empty,
                AST = printerOptions.IncludeAST ? PrintAST(rootNode) : string.Empty,
                ReorderedModifiers = reorderedModifiers,
                ReorderedUsingsWithDisabledText = reorderedUsingsWithDisabledText,
                MovedTrailingTrivia = movedTrailingTrivia,
            };
        }
        catch (InTooDeepException)
        {
            return new CodeFormatterResult
            {
                FailureMessage = "We can't handle this deep of recursion yet.",
            };
        }
    }

    private static string PrintAST(CompilationUnitSyntax rootNode)
    {
        try
        {
            var stringBuilder = new StringBuilder();
            SyntaxNodeJsonWriter.WriteCompilationUnitSyntax(stringBuilder, rootNode);
            // SyntaxNodeJsonWriter doesn't write things indented, so this cleans it up for us
            return JsonSerializer.Serialize(
                JsonSerializer.Deserialize<object>(stringBuilder.ToString()),
                IndentedJson
            );
        }
        // in some cases with new unsupported c# language features
        // SyntaxNodeJsonWriter will not produce valid json
        catch (JsonException ex)
        {
            return JsonSerializer.Serialize(new { exception = ex.ToString() }, IndentedJson);
        }
    }
}
